#!/usr/bin/env python
"""
Skaffold, a batteries-included test framework.

License: BSD
Project homepage: http://skaffold.googlecode.com
"""

import sys
import os
import traceback
from optparse import OptionParser

class Test():
    """A descriptor of a test to run.
    
    Test('testname', ClsName, *init_args, **init_kw) describes a test that will be run via
    ClsName(report_visitor, *init_args, **init_kw).run()."""
    def __init__(self, name, cls, *args, **kw):
        self.name = name
        self.cls = cls
        self.args = args
        self.kw = kw
    def __repr__(self):
        return repr({'name':self.name, 'cls':self.cls, 'args':self.args, 'kw': self.kw})
    def instantiate(self, visitor):
        try:
            return self.cls(visitor, *self.args, **self.kw)
        except Exception:
            result = Error('Could not instantiate test class', Result.Details(self.name))
            result.traceback = traceback.format_exc()
            raise result
    def run(self, visitor):
        try:
            self.instantiate(visitor).run()
        except Result, result:
            if not hasattr(result, 'traceback'):
                result.traceback = traceback.format_exc()
            raise result
        except Exception:
            result = Error('Could not execute run() method on test class', Result.Details(self.name))
            result.traceback = traceback.format_exc()       
            raise result 
        
class Suite(list):
    """An ordered list of tests to run."""
    def __setitem__(self, key, value):
        if not instanceof(value, Test):
            raise TypeError("A Suite may only contain Test's")
        list.__setitem__(self, key, value)
    def run_tests(self):
        # Add the SummaryReporter to the reporters list
        try:
            self.reporters.insert(0, SummaryReporter())
        except AttributeError:
            from skaffold.reporters.terminal_reporter import TerminalReporter
            reporters = [SummaryReporter(), TerminalReporter()]
        
        # Prepare the reporter(s)
        for reporter in reporters: 
            reporter.open()
            
        # Run the tests
        for test in self:
            visitor = ReportVisitor(reporters, [test.name])
            result = None
            try:
                test.run(visitor)
            except Result, result:
                if not hasattr(result, 'traceback'):
                    result.traceback = traceback.format_exc()
            except Exception:
                result = Error('Could not execute run() method on test class', Result.Details(test.name))
                result.traceback = traceback.format_exc()
            if result:
                visitor.report(result)

        # Close out the reporters
        for reporter in reporters:
            reporter.close()    

class Result(Exception):
    """An object representing the result of a test."""    
    def __init__(self, message, details = None):
        self.message = message
        self.details = details
    def attach_details(self, details):
        self.details = details
    class Details(dict):
        def __init__(self, testname, default_success_msg=""):
            self._default = default_success_msg
            self._testname = testname
        def __repr__(self):
            return repr({'testname':self._testname, 'default_success_msg':self._default, 'dict': dict(self)})
class Skipped(Result): key = "Skipped"; ran = False; passed = False; failed = False
class Error(Result):   key = "Error";   ran = False; passed = False; failed = True
class Failure(Result): key = "Failure"; ran = True;  passed = False; failed = True
class Success(Result): key = "Success"; ran = True;  passed = True;  failed = False

class Reporter():
    """A base class for a results reporter."""
    def open(self):
        pass
    def close(self):
        pass
    # Use two functions so that derived classes can skip the error-checking if they like
    def report(self, resultexc, tree):
        if not isinstance(resultexc, Result):
            raise TypeError("Reporter's only know how to process Result exception types (got %s instead)" % type(resultexc))
        self.report_handler(resultexc, tree)
    def report_handler(self, resultexc, tree):
        pass

class SummaryReporter(Reporter):
    """A base class for a results reporter."""
    def __init__(self):
        self.history = []
    def report_handler(self, resultexc, tree):
        self.history.append((tree,resultexc.key))

class ReportVisitor():
    """A visitor object passed to a Test that is used to report() the Result
    to the suite's Reporter's"""
    def __init__(self, reporters, tree):
        self.reporters = reporters
        self.tree = tree
    def report(self, result):
        for reporter in self.reporters:
            reporter.report(result, self.tree)
        
class Skaffold(Test):
    """The test framework driver."""
    __version = ('0','0','1')
    def __init__(self, args=None, context=None, listeners=None):
        self.globals = context if isinstance(context, dict) else dict()
        self.globals['suite'] = Suite()
        
        # Handle the options
        parser = OptionParser(usage="%prog [-C config1 [-C config2]", version="%prog " + ".".join(Skaffold.__version))
        parser.set_defaults(config_file=[], verbose=False)
        parser.add_option("--verbose","-V", action="store_true", dest="verbose", help="Verbose output (default %default)")
        parser.add_option("--config","-C", action="append", dest="config_file", help="Load a Skaffold configuration file (may be used repeatedly)")
        (self.options, self.args) = parser.parse_args(args)
    
    def process_config_files(self):
        """Process the config files in order passed"""
        for configfile in self.options.config_file:
            try:
                execfile(configfile, self.globals)
            finally:
                # @todo - Need error handling for bad config files
                pass
    
    def process_test_defs(self):
        """Create the test suite in order passed"""
        loc = dict()
        for testsuite in self.args:
            try:
                execfile(testsuite, self.globals)
            finally:
                # @todo - Need error handling for bad test def files
                pass
    
    def run(self):
        """Top-level delegator for the Skaffold class."""
        self.process_config_files()
        self.process_test_defs()
        exec "suite.run_tests()" in self.globals
